Repository navigation
Add a build-time opt-in hybrid API documentation POC - #1874
Draft
David Pine (IEvangelist) wants to merge 29 commits into
Draft
David Pine (IEvangelist) wants to merge 29 commits into
David Pine (IEvangelist) wants to merge 29 commits into
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
Frontend HTML artifact readyThe latest frontend build uploaded the This comment updates automatically when a new frontend build artifact is uploaded. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Hybrid mode serves plain files and forwards uncompressed renderer output, so pages, assets and sitemaps lost the precompression of the static build. Enable Brotli and gzip response compression for the hybrid pipeline. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The TypeScript API landing page embedded a 13 MB search index in its HTML. Landing, package and module pages now fetch a prerendered JSON file with a delayed loading state and an in-place retry when the download fails. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The name link was a flex item with min-width:auto and the inline code ignored overflow, so long signatures widened the page by over 1,000 px. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Do not forward query strings or an Authorization-driven cache bypass for pages - Share one render per URL while it is in flight - Route pages to a stable renderer by path hash, with a failure cooldown - Answer matching validators with 304 and serve pages with an ETag and no-cache - Serve the site 404 page for unknown API URLs and a retry page for 502/504 - Keep the two API landing pages prerendered - Bound renderer heap and decouple the App Service health check from renderer readiness Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paths outside the on-demand manifest now fall through to static files, and the site 404 page is the fallback for unknown API URLs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
UseStaticFiles refuses unknown extensions, so Pagefind's index files, its WASM and the terminal recordings returned 404. The acceptance script now requests one file per extension in the client output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The hybrid model forced the plan to 2 instances and the site to 2 workers, which would override the existing 3 automatically scaled instances. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
App Service resolves a sidecar environment variable's value as the name of an app setting, so the literal PORT values were ignored and both renderers bound 4323, which stopped the whole site from starting. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The lockfile was regenerated from the merged manifest, and the duplicate member-name overflow CSS was reconciled with the base branch's fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The earlier merge resolution recorded tarball URLs from a corporate proxy, which the supply-chain policy check rejects. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a build-time opt-in hybrid Astro/Starlight POC. With
ASPIRE_HYBRID_API_DOCS=1, non-API documentation stays static and C# / TypeScript API HTML and Markdown render on demand using the official Node adapter. The default build and static Azure deployment remain unchanged.Targets
release/17.0so the feature can ship with 17.0. It depends on an Aspire CLI change, tracked in microsoft/aspire#20967 (see below).Authorizationheaders are not forwarded for pages, so they cannot multiply or bypass cached renders. Concurrent requests for one cold page share a single render. Pages are routed to a renderer by a stable hash of their path, so each worker keeps its own half of the cache warm; a failed renderer is skipped for 10 seconds but kept as a last resort. Responses carry a weak ETag derived from the build id and path withCache-Control: no-cache, and a matchingIf-None-Matchis answered with 304 without calling the renderer. Unknown API URLs get the site 404 page, and 502/504 get a retry page withRetry-After./healthz/api-docs. Losing both renderers produces explicit API gateway errors while static pages remain available; App Service health checks use/healthzso a renderer outage does not make the instance unhealthy, while warmup still waits for renderer readiness. Renderers run with a 1 GiB heap limit.Dockerfile.hybridpackages the Astro server, production runtime dependencies, local image inputs and highlighting metadata. Supply a prebuilt immutable renderer image throughapi-docs-image; Aspire publishes the .NET image. Both images must come from the same Linux artifact.HybridApiDocsArtifactDirectoryselects an artifact outsidedist-hybrid.sitemap-0.xmlfrom 14.5 MB to 752 KB (176 KB). The trade-off is per-request CPU (about 20 ms for the 13.9 MB page at Brotli quality 1) and output roughly 1.5x to 4x larger than build-time maximum quality. Precompressing the hybrid client would close that gap.Aspire CLI dependency
aspire docs api list|search|getbuilds its catalog only fromhttps://aspire.dev/sitemap-0.xml. In hybrid mode Astro's generatedsitemap-0.xmlno longer lists the API routes, because they are not prerendered (only the 2 prerendered landing pages remain, instead of about 24,000 URLs). The build therefore writes a separatesitemap-api.xmlwith every on-demand API HTML URL (about 24,200) and links it fromsitemap-index.xml.The CLI must ingest the catalog from the sitemap index. That change, the exact ingestion steps and an open question about older CLIs are in microsoft/aspire#20967. Until it ships,
listandsearchagainst a hybrid-built site return almost nothing, whilegetfor a known page works.The model was deployed to Azure in a test subscription (West US 2) with a single
aspire deployafter a one-time step that puts the renderer image in the registry. It runs as a .NET main container plus two Astro sidecars on a P0v3 plan, shaped like production (elastic scale, 2 minimum and 1 pre-warmed instance); two instances were ready, both renderers started on ports 4323 and 4324 on each, and the homepage, static and on-demand API pages, the search index, Pagefind, terminal recordings, the 404 page and/healthz/api-docsall returned the expected responses. The first deploy exposed a real bug: App Service treats a sidecar environment variable's value as the name of an app setting, so the literalPORTvalues were ignored, both renderers bound 4323 and the whole site returned 503. Each sidecar now gets its port from its own app setting, with a test. Still unverified: automatic scale-out beyond the minimum, slot swaps with sidecars, behavior under production load, and the Azure DevOps and EV2 pipelines, which need a Linux build job and a second image-push step.Measured POC result
The native-cache acceptance run on
502c9419passed using the process-based harness, before the independent-container packaging revision:It avoided 48,451 static API HTML/Markdown files and preserved 8,828 non-API outputs. The private server bundle was 123,256,415 bytes. These are measurements of that revision, not the latest container images; hybrid mode still had higher peak build memory.
Third-party links and affiliations
None. This change does not add or change third-party links.
Validation
Independent containers. Run 38416394011 built the static baseline, the hybrid artifact and both images, and passed output parity, but the container check failed because the Astro image was missing runtime dependencies. Commit
55f9ba5bfixed that. Run 38426759110 rechecked the images against that run's Linux artifact and passed: separate containers with no mounts and a shared network namespace, native cache MISS then HIT, primary failover, total renderer loss, and recovery. Commits after55f9ba5b(45d688f9) are covered by the unit and .NET checks below but not by a full CI run.Merged result on
release/17.0. The merge of this branch withrelease/17.0(31 newer commits) is clean. In a throwaway worktree of that merge:src/frontend:pnpm install --frozen-lockfile --prefer-offline— passed.src/frontend:pnpm exec astro sync— passed.src/frontend:pnpm exec vitest run --config vitest.config.ts tests/unit/hybrid-api-docs.vitest.test.ts tests/unit/hybrid-native-cache.vitest.test.ts tests/unit/api-markdown.vitest.test.ts tests/unit/ts-api-route-catalog.vitest.test.ts tests/unit/build-performance.vitest.test.ts— passed, 61 tests and one existing skip.dotnet test .\tests\Aspire.Dev.AppHost.Tests\Aspire.Dev.AppHost.Tests.csproj --nologo --verbosity minimal— passed, 11 tests, including generated Bicep, sidecar topology, managed identity, ports and readiness settings.dotnet test .\tests\StaticHost.Tests\StaticHost.Tests.csproj --nologo --verbosity minimal --filter 'FullyQualifiedName!~Redis'— passed, 315 tests.Compression fix. Commits
ecbdc0cdand5f6063c0add the response compression above. From the repository root:dotnet test .\tests\StaticHost.Tests\StaticHost.Tests.csproj --nologo --verbosity minimal --filter 'FullyQualifiedName!~Redis'— passed, 319 tests (including Brotli and gzip round-trips for forwarded and static responses);dotnet test .\tests\Aspire.Dev.AppHost.Tests\Aspire.Dev.AppHost.Tests.csproj --nologo --verbosity minimal— passed, 11 tests. Both ran in a throwaway worktree of this branch rather than therelease/17.0merge. Sizes above were measured on the local preview after rebuilding StaticHost.Earlier local checks.
pnpm exec eslinton the changed hybrid files passed.pnpm exec tsc --noEmit --pretty falsefails on existing repository diagnostics, with none reported in the changed hybrid files. The preceding native-cache CI run passed Linux acceptance and Windows artifact/contracts checks, includingpnpm exec playwright test --config playwright.hybrid.config.mjs. A local isolated Aspire preview using the Windows CI artifact served the homepage, API pages and/healthz/api-docswith both renderers healthy.API search indexes: the landing, package and module pages embedded their search index in the HTML (13.3 MB on the TypeScript landing, in production too). They now fetch prerendered
/reference/api-search/{csharp,typescript}[/slug].jsonfiles (outsidereference/api/, so they stay static in hybrid mode), with a delayed "Loading search index…" state and an in-place Try again button when the download fails. The TypeScript landing HTML drops from 13.6 MB to 298 KB and all API pages from 105.6 MB to 70.6 MB; the type/item pages keep their tiny inline indexes. All 334 generated indexes (95,333 entries) are byte-identical to the previous inline ones. Validated with eslint, the full frontend unit suite (80 files, 1,025 tests) and the affected desktop Chromium e2e tests (11 passed).verify-hybrid-output.mjsnow asserts the index files stay prerendered.Hardening from a design review. Measured against a local preview of the Windows CI artifact: query strings and
Authorizationforced a render on every request (now cache hits of about 8 ms); 12 concurrent requests for one cold page caused 12 renders (now 1); a burst of 24 renders raised a cache hit from 8 ms to 221 ms (now a median of 9 ms); 80 distinct pages split 45/35 across the two renderers; revalidation returns 304 with no renderer request; and unknown API URLs return the 157 KB site 404 page instead of an empty body. The landing pages were the one expensive case: 40 concurrent landing renders took a 5.2 s median, grew a worker to 1.4 GB RSS and pushed a health probe to the 2 s timeout. With the landings prerendered the same flood takes a 0.5 s median, worker memory stays near 440 MB, and the health probe peaks at 140 ms. Package, module, type and member pages render in a median of 22-31 ms, so they stay on demand. Run 38503203899 passed the whole hybrid pipeline on1ffec64c, including output parity (landings prerendered, everything else on demand), the independent-container acceptance script (stampede, ETag 304, query and credential handling, 404 page, retry page, failover by path ownership, total outage with/healthzstaying 200, and recovery) and the browser tests. Locally:dotnet testfor StaticHost (329, non-Redis) and AppHost (11) in a throwaway worktree, the hybrid frontend unit tests, and eslint on the changed files.Outstanding: the Aspire CLI change in microsoft/aspire#20967; Azure deployment, cloud restart behavior and production load were not tested; the
aspire docs apicommands were not run against a hybrid site. The broader frontend gate on this PR had an unrelated failure indashboard-image-references.vitest.test.ts(a volume-mount example) when it ran againstmain; neither that test nor dashboard documentation changed in this PR. No full-site build was run locally.